package org.speedy;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.speedy.analysis.AnalysisResult;
import org.speedy.analysis.TypeExtractor;
import org.speedy.classpath.ClasspathInterogator;
import org.speedy.tree.DependencyTree;

//TODO - review whether returning empty visitors breaks tests
//TODO - review other visitor methods and check if they are required
public class Speedy implements Runnable {

    private final TypeExtractor TYPE_EXTRACTOR = new TypeExtractor();
	private final DependencyTree tree = new DependencyTree();
    private final Queue<String> discoveries = new LinkedList<String>();
    private final HashSet<String> analysis = new HashSet<String>();

    private final ClassVisitor FINDER = new ClassReferenceFinder();

    private final String packageFilter;
	private final List<String> changedClasses;

    public Speedy(String testFilter, String packageFilter, List<String> changedClasses) throws Exception {
        this.packageFilter = packageFilter;
		this.changedClasses = changedClasses;
		discoveries.addAll(new ClasspathInterogator(testFilter).findTests());
		log("Number of tests found: " + discoveries.size());
		log("Tests found: " + discoveries);
    }

    public void run() {
        while(!discoveries.isEmpty()) {
            String next = discoveries.poll();
            new ClassDiscoverer().run(next);
        }
    }

    private void addDescription(String className, String desc) {
        if (desc.contains("(") && desc.contains(")")) {
            String[] types = TYPE_EXTRACTOR.processMethod(desc);
            for (String t : types) {
                if (t != null) addType(className, t);
            }
        } else {
            String type = TYPE_EXTRACTOR.processType(desc);
            if (type != null) addType(className, type);
        }
    }

    private void addType(String className, String dependency) {
        if (dependency.matches(packageFilter)) {
            discoveries.add(dependency);
            tree.add(className, dependency, this.changedClasses.contains(dependency));
        }
    }

    /**
     * Find class references in Annotations.
     */
    class AnnotationReferenceFinder implements AnnotationVisitor {
        private final String className;

        public AnnotationReferenceFinder(String className) {
            this.className = className;
        }

        public void visit(String arg0, Object arg1) {
            // this could be a Class reference, so we should try to add it
            addDescription(className, arg1.toString());
        }

        public AnnotationVisitor visitAnnotation(String name, String desc) {
        	//TODO test!
            addDescription(className, desc);
            return this;
        }

        public AnnotationVisitor visitArray(String arg0) {
        	//TODO needs implementation?
            return this;
        }

        public void visitEnd() {
        }

        public void visitEnum(String name, String desc, String value) {
        	//TODO commenting out this doesn't break the tests
            addDescription(className, desc);
        }
    }

    /**
     * Find class references in Field declarations.
     */
    class FieldReferenceFinder implements FieldVisitor {
        private final String className;

        public FieldReferenceFinder(String className) {
            this.className = className;
        }

        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            addDescription(className, desc);
            return new AnnotationReferenceFinder(className);
        }

        public void visitAttribute(Attribute arg0) {
            //TODO: Needs implementation?
        }

        public void visitEnd() {
        }
    }

    /**
     * Find class references in Method implementations.
     */
    class MethodReferenceFinder implements MethodVisitor {

        private final String className;

        public MethodReferenceFinder(String className) {
            this.className = className;
        }

        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            addDescription(className, desc);
            return new AnnotationReferenceFinder(className);
        }

        public AnnotationVisitor visitAnnotationDefault() {
            return new AnnotationReferenceFinder(className);
        }

        public void visitAttribute(Attribute arg0) {
            //TODO: needs implementation?
        }

        public void visitCode() {
        }

        public void visitEnd() {
        }

        public void visitFieldInsn(int op, String owner, String name, String desc) {
        	//TODO below doesn't break the tests
            addType(className, owner);
            //TODO below doesn't break the tests
            addDescription(className, desc);
        }

        public void visitFrame(int arg0, int arg1, Object[] arg2, int arg3, Object[] arg4) {
        }
        public void visitIincInsn(int arg0, int arg1) {
        }
        public void visitInsn(int arg0) {
        }
        public void visitIntInsn(int arg0, int arg1) {
        }
        public void visitJumpInsn(int arg0, Label arg1) {
        }
        public void visitLabel(Label arg0) {
        }

        public void visitLdcInsn(Object insn) {
            if (insn instanceof Type) {
                addDescription(className, insn.toString());
            }
        }

        public void visitLineNumber(int arg0, Label arg1) {
        }

        public void visitLocalVariable(String name, String desc, String sig, Label start, Label end, int index) {
        	//TODO below doesn't break the tests
            addDescription(className, desc);
        }

        public void visitLookupSwitchInsn(Label arg0, int[] arg1, Label[] arg2) {
        }
        public void visitMaxs(int arg0, int arg1) {
        }

        public void visitMethodInsn(int op, String owner, String name, String desc) {
            if (owner.startsWith("[") && owner.charAt(1) != 'L') return;
            if (owner.endsWith(";")) {
                //TODO below doesn't break the tests
            	addDescription(className, desc);
            } else {
            	//TODO below doesn't break the tests
                addType(className, owner);
            }
        }

        public void visitMultiANewArrayInsn(String type, int arg1) {
        	//TODO below doesn't break the tests
            addDescription(className, type);
        }

        public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) {
        	//TODO below doesn't break the tests
            addDescription(className, desc);
            return new AnnotationReferenceFinder(className);
        }

        public void visitTableSwitchInsn(int arg0, int arg1, Label arg2, Label[] arg3) {
        }

        public void visitTryCatchBlock(Label arg0, Label arg1, Label arg2, String type) {
        	//TODO below doesn't break the tests
            if (type != null) addType(className, type);
        }

        public void visitTypeInsn(int arg0, String type) {
        	//TODO below doesn't break the tests
            addDescription(className, type);
        }

        public void visitVarInsn(int arg0, int arg1) {
        }
    }

    /**
     * Find class references in Class descriptions.
     */
    class ClassReferenceFinder implements ClassVisitor {
        private String className;

        public void visit(int ver, int access, String name, String sig, String supr, String[] ifcs) {
            this.className = name;
            //Note: we do not need to add 'name' because visiting it implies we've already
            //added it to our work queue.
            //TODO below doesn't break the tests
            if (supr != null) addType(className, supr);
            for (String ifc : ifcs) {
            	//TODO below doesn't break the tests
                addType(className, ifc);
            }
        }

        public AnnotationVisitor visitAnnotation(String desc, boolean arg1) {
            addDescription(className, desc);
            return new AnnotationReferenceFinder(className);
        }

        public void visitAttribute(Attribute arg0) {
            //TODO: needs implementation?
        }

        public void visitEnd() {
        }

        public FieldVisitor visitField(int access, String name, String desc, String sig, Object value) {
            addDescription(className, desc);
            return new FieldReferenceFinder(className);
        }

        public void visitInnerClass(String name, String outer, String inner, int access) {
            if (name != null) addType(className, name);
            //TODO below doesn't break the tests
            if (outer != null) addType(className, outer);
        }

        public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] expts) {
            if (desc != null && !name.equals("<init>")) addDescription(className, desc);

            if (expts != null) {
                for (String expt : expts) {
                	//TODO below doesn't break the tests
                    addType(className, expt);
                }
            }

            return new MethodReferenceFinder(className);
        }

        public void visitOuterClass(String owner, String name, String desc) {
        	//TODO below doesn't break the tests
            if (owner != null) addType(className, owner);
        }

        public void visitSource(String arg0, String arg1) {
        }
    }

    class ClassDiscoverer {
        public void run(String next) {
            if (analysis.contains(next)) {
            	return;
            }

            analysis.add(next);

            try {
                ClassReader cr = new ClassReader(next);
                cr.accept(FINDER, ClassReader.EXPAND_FRAMES);
                logProgress(".");
            } catch (IOException e) {
            	log("Cannot load class: " + next);
            }
        }
    }

    private static int progress = 1;
	private static boolean verbose = false;

	private void logProgress(String text) {
		if (!verbose) {
			return;
		}

		progress++;
		if (progress%250 == 0) {
			System.out.print(text);
		}
	}

	private static List<String> findTests(List<String> changedClasses) {
		List<String> tests = new ArrayList<String>();
		for (String changedClass : changedClasses) {
			if (changedClass.endsWith("Test")) {
				tests.add(changedClass);
			}
		}
		return tests;
	}

    public static void main(String ... args) throws Exception {
    	execute(args);
    }

	public static AnalysisResult execute(String ... args) throws Exception {
		SpeedyConfig config = new SpeedyConfig(args);

		List<String> changedClasses = config.getChangedClasses();
    	verbose = config.isVerbose();

		List<String> changedTests = findTests(changedClasses);

    	log("Building test suite for:");
    	log(changedClasses);
    	log("Processing...");

        Speedy speedy = new Speedy(config.getTestFilter(), config.getPackageFilter(), changedClasses);
        long before = System.currentTimeMillis();
        speedy.run();
        long after = System.currentTimeMillis();
        log("");

        Set<String> testsToRun = speedy.tree.matchingLeaves(config.getTestFilter());
        testsToRun.addAll(changedTests);

        log("All tests to run: ");

		for(String test : testsToRun) {
			System.out.println(test);
		}

		log("Time of analysis in seconds: " + (after - before) / 1000);
		log("Classes analyzed: " + speedy.analysis.size());
		log("Tests to run: " + testsToRun.size());

		return new AnalysisResult(speedy.analysis, testsToRun);
	}

	private static void log(Object text) {
		if (verbose) {
			System.out.println(text);
		}
	}
}